package devcken.taglibrary;

import java.io.File;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.MessageSource;
import org.springframework.web.servlet.LocaleResolver;

/**
 * 열거형 태그를 관리하기 위한 클래스로 설정된 범위 내에서 열거형 태그 어노테이션에 의해 지정된 열거형 태그들을 관리한다.
 * @author Devcken
 */
public class EnumerationTagLoader
{
	private static final Logger logger = LoggerFactory.getLogger(EnumerationTagLoader.class);
	
	/**
	 * 관리할 열거형에 대한 최상위 패키지 명칭을 나타낸다.
	 */
	private String base;
	
	/**
	 * 열거형 태그의 i18n(국제화) 적용을 위한 메시지 소스를 나타낸다.
	 */
	private MessageSource _messageSource;
	
	/**
	 * HTTP 헤더에 포함된 로컬화 정보를 통해 로컬 정보를 제공한다.
	 */
	private LocaleResolver _localeResolver;
	
	/**
	 * 범위 내에서 로드된 열거형 태그의 목록을 나타낸다.
	 */
	private HashMap<String, EnumerationTag> _tags;
	
	public EnumerationTagLoader()
	{
		this._tags = new HashMap<String, EnumerationTag>();
	}

	/**
	 * 열거형 태그를 메모리에 로드한다.
	 */
	public void load()
	{
		try
		{
			// 범위 내의 클래스 목록을 모두 가져온다.
			Class<?>[] classes = this.getClasses(this.base);
			
			for (Class<?> cls : classes)
			{
				// 클래스가 열거형인 경우에만 처리한다.
				if (cls.isEnum())
				{
					// 열거형의 어노테이션 목록을 가져온다.
					Annotation[] annotations = cls.getDeclaredAnnotations();
					
					for (Annotation annotation : annotations)
					{
						// 어노테이션이 열거형 태그 지정 어노테이션인 경우에만 처리한다.
						if (annotation instanceof EnumerationTag)
						{
							EnumerationTag tagAnnotation = (EnumerationTag)annotation;
							
							EnumerationTag tag = new EnumerationTag();
							
							// 어노테이션에 지정된 명칭으로 열거형 태그의 명칭을 설정한다.
							tag.setName(tagAnnotation.name);
							
							// 클래스(열거형)로부터 필드 목록(열거형 상수)을 가져온다.
							Field[] fields = cls.getFields();
							
							// 열거형을 관리 열거형으로 변환한다.
							HashMap<String, devcken.taglibrary.Enumeration> enumerationsMap = this.getEnumerationsMap((Enum<?>[])cls.getEnumConstants());
							
							for (Field field : fields)
							{
								// 열거형 상수의 어노테이션 목록을 가져온다.
								Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
								
								for (Annotation fieldAnnnotation : fieldAnnotations)
								{
									// 어노테이션이 열거형 태그 상수 속성인 경우에만 처리한다.
									if (fieldAnnnotation instanceof EnumerationTagAttribute)
									{
										EnumerationTagAttribute attributeAnnotation = (EnumerationTagAttribute)fieldAnnnotation;
										
										// 열거형 상수 값을 얻기 위해 관리 열거형 맵에서 상수에 해당되는 관리 열거형을 가져온다. 
										devcken.taglibrary.Enumeration enumeration = enumerationsMap.get(field.getName());
										
										EnumerationTagAttribute attribute = new EnumerationTagAttribute();
										
										attribute.setName(attributeAnnotation.name);
										attribute.setValue(String.valueOf(enumeration.value()));
										attribute.setGroup1(attributeAnnotation.group1);
										attribute.setGroup2(attributeAnnotation.group2);
										attribute.setGroup3(attributeAnnotation.group3);
										
										// 열거형 태그 상수 속성을 만들어 열거형 태그에 설정한다.
										tag.addAttribute(attribute);
										
										break;
									}
								}
							}
							
							this._tags.put(tag.getName(), tag);
							
							break;
						}
					}
				}
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
	}
	
	/**
	 * 특정 패키지의 모든 하위 클래스 목록을 가져온다.
	 * @param packageName 얻고자 하는 하위 클래스 목록의 최상위 패키지 명을 설정한다.
	 * @return 특정 패키지의 모든 하위 클래스 목록을 반환한다.
	 */
	private Class<?>[] getClasses(String packageName)
	{
		ArrayList<Class<?>> classes = new ArrayList<Class<?>>();
		
		try
		{
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			
			assert classLoader != null;
		
			String path = packageName.replace('.', '/');
			
			Enumeration<URL> resources = classLoader.getResources(path);
			
			List<File> dirs = new ArrayList<File>();
			
			while (resources.hasMoreElements())
			{
				URL resource = resources.nextElement();
				
				dirs.add(new File(resource.getFile()));
			}
		
			for (File directory : dirs)
			{
				classes.addAll(findClasses(directory, packageName));
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return classes.toArray(new Class[classes.size()]);
	}
	
	/**
	 * 특정 디렉토리에서 패키지명에 해당하는 모든 클래스 목록을 가져온다.
	 * @param directory 가져오려는 클래스 목록의 최상위 디렉토리를 설정한다.
	 * @param packageName 가져오려는 클래스 목록의 최상위 패키지명을 설정한다.
	 * @return 하위 디렉토리를 포함하여 특정 디렉토리에 존재하는 모든 클래스 목록을 반환한다. 
	 */
	private List<Class<?>> findClasses(File directory, String packageName)
	{
		List<Class<?>> classes = new ArrayList<Class<?>>();

		try
		{
			// 경로가 존재 하지 않는 경우, 더 이상 진행할 수 없다.
			if (!directory.exists())
			{
				return classes;
			}
	
			// 경로에 속하는 모든 파일 목록을 가져온다.
			File[] files = directory.listFiles();
	
			for (File file : files)
			{
				// 만약 디렉토리인 경우, 디렉토리 하위에 속해 있는 파일들을 재귀적으로 조사한다. 
				if (file.isDirectory())
				{
					assert !file.getName().contains(".");
					
					classes.addAll(findClasses(file, packageName + "." + file.getName()));
				}
				// 클래스 파일이고 내부 클래스가 아닌 경우, 패키지 명칭과 클래스명(파일명)으로 클래스를 가져온다. 
				else if (file.getName().endsWith(".class") && !file.getName().contains("$"))
				{
					classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6)));
				}
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}

		return classes;
	}
	
	/**
	 * 열거형을 관리 대상 열거형으로 변환하여 열거형 명칭을 키로 하는 맵으로 가져온다. 
	 * @param enumerations 열거형 목록을 설정한다.
	 * @return 열거형 명칭을 키로 하는 열거형 맵을 반환한다.
	 */
	private HashMap<String, devcken.taglibrary.Enumeration> getEnumerationsMap(Enum<?>[] enumerations)
	{
		HashMap<String, devcken.taglibrary.Enumeration> result = new HashMap<String, devcken.taglibrary.Enumeration>();
		
		try
		{
			for (Enum<?> item : enumerations)
			{
				// 열거형 인터페이스를 구현한 열거형만 처리한다.
				if (item instanceof Enumeration)
				{
					// 열거형을 관리 대상 열거형으로 변환한다.
					devcken.taglibrary.Enumeration enumeration = (devcken.taglibrary.Enumeration)item;
					
					result.put(enumeration.name(), enumeration);
				}
			}
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
		}
		
		return result;
	}
	
	/**
	 * 열거형 태그 명칭에 해당하는 열거형 태그를 가져온다.
	 * @param name 가져오려는 열거형 태그의 명칭을 설정한다.
	 * @return 열거형 태그 명칭에 해당하는 열거형 태그를 반환한다.
	 */
	public EnumerationTag getTag(String name)
	{
		return this._tags.get(name);
	}
	
	/**
	 * 열거형 태그 명칭과 상수 값에 해당하는 열거형 태그의 상수의 명칭으로 i18n 메시지를 가져온다.
	 * 열거형 태그와 
	 * @param code 열거형 태그의 명칭을 설정한다.
	 * @param value 열거형 태그의 상수값을 설정한다.
	 * @param request HTTP 요청으로 헤더의 지역화 정보를 기대한다.
	 * @return 
	 */
	public String getTagValueByI18n(String code, String value, HttpServletRequest request)
	{
		try
		{
			// HTTP 요청에 따른 지역 정보를 가져온다.
			Locale locale = _localeResolver.resolveLocale(request);
			
			// 명칭과 값에 해당하는 열거형 태그 속성을 가져온다.
			EnumerationTagAttribute attribute = this.getTag(code).getAttribute(value);
			
			if (attribute == null)
			{
				return "";
			}
		
			// 상수 명칭에 해당하는 i18n 메시지를 지역 정보에 맞게 가져온다.(지역 정보에 맞는 메시지가 없는 경우, 기본으로 설정된 지역 정보의 메시지를 가져온다.)
			return this._messageSource.getMessage(attribute.getName(), null, locale);
		}
		catch (Exception error)
		{
			logger.error(error.getMessage(), error);
			
			return "";
		}
	}

	public String getBase()
	{
		return base;
	}

	public void setBase(String base)
	{
		this.base = base;
	}

	public MessageSource getMessageSource()
	{
		return _messageSource;
	}

	public void setMessageSource(MessageSource _messageSource)
	{
		this._messageSource = _messageSource;
	}

	public LocaleResolver getLocaleResolver()
	{
		return _localeResolver;
	}

	public void setLocaleResolver(LocaleResolver _localeResolver)
	{
		this._localeResolver = _localeResolver;
	}
}